package com.gitee.ixtf.vertx.guice

import com.gitee.ixtf.guice.Jguice
import com.gitee.ixtf.vertx.Subscription
import com.gitee.ixtf.vertx.classInfoList
import com.gitee.ixtf.vertx.handler.GraphqlExceptionHandler
import com.gitee.ixtf.vertx.internal.*
import com.gitee.ixtf.vertx.internal.converter.registerDataLoaderName
import com.gitee.ixtf.vertx.internal.internal.JsonObjectAdapterInstrumentation
import com.google.inject.AbstractModule
import com.google.inject.Provides
import com.google.inject.Singleton
import graphql.GraphQL
import graphql.execution.instrumentation.ChainedInstrumentation
import graphql.execution.preparsed.persisted.ApolloPersistedQuerySupport
import graphql.execution.preparsed.persisted.InMemoryPersistedQueryCache.newInMemoryPersistedQueryCache
import graphql.scalars.ExtendedScalars
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import io.github.classgraph.ClassGraph
import io.vertx.ext.web.handler.graphql.UploadScalar
import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.flatMap
import org.dataloader.BatchLoader
import org.dataloader.BatchPublisher
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory.newDataLoader
import org.dataloader.DataLoaderFactory.newMappedDataLoader
import org.dataloader.DataLoaderFactory.newMappedPublisherDataLoader
import org.dataloader.DataLoaderFactory.newPublisherDataLoader
import org.dataloader.DataLoaderOptions
import org.dataloader.DataLoaderRegistry
import org.dataloader.MappedBatchLoader
import org.dataloader.MappedBatchPublisher
import org.dataloader.registries.ScheduledDataLoaderRegistry
import org.eclipse.microprofile.graphql.GraphQLApi
import org.eclipse.microprofile.graphql.Mutation
import org.eclipse.microprofile.graphql.Query
import org.eclipse.microprofile.graphql.Source

open class GraphqlGuice(vararg packages: Class<*>) : AbstractModule() {
  private val classList by lazy {
    classInfoList(*packages) {
      buildList {
        add(getClassesWithAnnotation(GraphQLApi::class.java))
        add(getClassesImplementing(MappedBatchLoader::class.java))
        add(getClassesImplementing(MappedBatchPublisher::class.java))
        add(getClassesImplementing(BatchLoader::class.java))
        add(getClassesImplementing(BatchPublisher::class.java))
      }
    }
  }
  private val loaderMap by lazy {
    classList
        .filter {
          BatchLoader::class.java.isAssignableFrom(it) ||
              BatchPublisher::class.java.isAssignableFrom(it) ||
              MappedBatchLoader::class.java.isAssignableFrom(it) ||
              MappedBatchPublisher::class.java.isAssignableFrom(it)
        }
        .onEach { registerDataLoaderName(it) }
        .associate { it.name to Jguice.getInstance(it) }
  }
  protected open val dlSes: ScheduledExecutorService by lazy {
    Executors.newSingleThreadScheduledExecutor()
  }

  // protected val dlSes by lazy { Executors.newScheduledThreadPool(DEFAULT_EVENT_LOOP_POOL_SIZE) }

  override fun configure() {
    bind(DataLoaderRegistry::class.java).toProvider { buildDataLoaderRegistry(loaderMap) }
  }

  private fun methodList(): Collection<GraphqlMethod> =
      classList
          .flatMap { it.declaredMethods.asIterable() }
          .mapNotNull { method ->
            when {
              method.isAnnotationPresent(Subscription::class.java) -> SubscriptionMethod(method)
              method.isAnnotationPresent(Mutation::class.java) -> MutationMethod(method)
              method.isAnnotationPresent(Query::class.java) ->
                  method.parameters
                      .filter { it.isAnnotationPresent(Source::class.java) }
                      .run {
                        if (size > 1) throw RuntimeException("GraphQL Source 重复 [$method]")
                        if (isNotEmpty()) SourceMethod(method, first()) else QueryMethod(method)
                      }
              else -> null
            }
          }
          .groupBy { "${it.typeName}:${it.fieldName}" }
          .map { (address, dataFetchers) ->
            if (dataFetchers.size > 1) throw RuntimeException("GraphQL 地址重复 [$address]")
            dataFetchers.first()
          }

  @Singleton
  @Provides
  private fun GraphQL(): GraphQL {
    val schemaParser = SchemaParser()
    val typeDefinitionRegistry = TypeDefinitionRegistry()
    ClassGraph().acceptPaths("graphql").scan().use {
      it.getResourcesWithExtension("graphqls").forEachByteArrayThrowingIOException { _, content ->
        val parse = schemaParser.parse(content.toString(UTF_8))
        typeDefinitionRegistry.merge(parse)
      }
    }
    val runtimeWiring =
        RuntimeWiring.newRuntimeWiring().run {
          scalar(UploadScalar.build())
          scalar(ExtendedScalars.GraphQLLong)
          scalar(ExtendedScalars.GraphQLShort)
          scalar(ExtendedScalars.GraphQLByte)
          scalar(ExtendedScalars.GraphQLBigDecimal)
          scalar(ExtendedScalars.GraphQLBigInteger)
          scalar(ExtendedScalars.GraphQLChar)
          scalar(ExtendedScalars.PositiveInt)
          scalar(ExtendedScalars.NegativeInt)
          scalar(ExtendedScalars.NonPositiveInt)
          scalar(ExtendedScalars.NonNegativeInt)
          scalar(ExtendedScalars.PositiveFloat)
          scalar(ExtendedScalars.NegativeFloat)
          scalar(ExtendedScalars.NonPositiveFloat)
          scalar(ExtendedScalars.NonNegativeFloat)
          scalar(ExtendedScalars.Url)
          scalar(ExtendedScalars.Locale)
          scalar(ExtendedScalars.Date)
          scalar(ExtendedScalars.Time)
          scalar(ExtendedScalars.DateTime)
          scalar(ExtendedScalars.Object)
          scalar(ExtendedScalars.Json)
          methodList().forEach { it.builder(this) }
          build(this)
          build()
        }
    return SchemaGenerator()
        .run { makeExecutableSchema(typeDefinitionRegistry, runtimeWiring) }
        .run { GraphQL.newGraphQL(this) }
        .run {
          defaultDataFetcherExceptionHandler(GraphqlExceptionHandler)
          instrumentation(
              ChainedInstrumentation(
                  ToCompletionStageInstrumentation,
                  JsonObjectAdapterInstrumentation,
                  // TODO: vertx 5.x，替换 JsonObjectAdapterInstrumentation
                  // JsonObjectAdapter(),
              ))
          preparsedDocumentProvider(
              ApolloPersistedQuerySupport(newInMemoryPersistedQueryCache().build()))
          build(this)
          build()
        }
  }

  protected open fun build(builder: RuntimeWiring.Builder) = Unit

  protected open fun build(builder: GraphQL.Builder) = Unit

  protected open fun buildDataLoaderRegistry(loaderMap: Map<String, Any>): DataLoaderRegistry =
      ScheduledDataLoaderRegistry.newScheduledRegistry().run {
        loaderMap.forEach { (name, loader) -> register(name, buildDataLoader(name, loader)) }
        scheduledExecutorService(dlSes)
        tickerMode(true)
        build()
      }

  protected open fun buildDataLoader(name: String, loader: Any): DataLoader<*, *> {
    // val options = DataLoaderOptions.newOptions().setBatchingEnabled(false).build()
    val options = DataLoaderOptions.newOptions().build()
    return when (loader) {
      is MappedBatchLoader<*, *> -> newMappedDataLoader(name, loader, options)
      is MappedBatchPublisher<*, *> -> newMappedPublisherDataLoader(name, loader, options)

      is BatchLoader<*, *> -> newDataLoader(name, loader, options)
      is BatchPublisher<*, *> -> newPublisherDataLoader(name, loader, options)
      else -> throw UnsupportedOperationException("DataLoader $loader")
    }
  }
}
